package main

import (
	"crypto/md5"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"strings"
	"sync"
	"time"

	"golang.org/x/net/context"

	"../pb"
	"../utils"
	grpclb "github.com/liyue201/grpc-lb"
	"google.golang.org/grpc"
	"google.golang.org/grpc/naming"
)

/*
   must be thread safe
*/
type Auth interface {
	AuthUserPass(string, string, string) bool          //user pass
	ChangePassTo(string, string, string, string) error //user pass
	AuthKey(string, string, string) bool               //user addr key
	AddKey(string, string, string) error               //user addr key
	Reload() error
	Unload() error
}

type Secret struct {
	Salt   string `json:"salt"`
	Shadow string `json:"shadow"`
}

type LocalAuth struct {
	mu      sync.RWMutex
	keyFile string
	keys    map[string]map[string]string //user ip key
	tkvalid map[string]int64
	rpvalid map[string][]string
}

func NewLocalAuth(key string) (*LocalAuth, error) {
	auth := &LocalAuth{keyFile: key}
	if err := auth.Reload(); err != nil {
		return nil, err
	}
	return auth, nil
}

func (ja *LocalAuth) ChangePassTo(user, addr, passwd, newpass string) error {
	if passwd == "" {
		return fmt.Errorf("null password")
	}

	ch := make(chan *naming.Update, 1)
	re := utils.NewResolver(ch)
	b := grpclb.NewBalancer(re, grpclb.NewRoundRobinSelector())
	ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
	conn, connerr := grpc.DialContext(ctx, *auth, grpc.WithInsecure(), grpc.WithBalancer(b))
	//	conn, connerr := grpc.Dial("10.19.156.37:50051", grpc.WithInsecure())

	if connerr != nil {
		log.Infof("change password fail because %v. request %s from %s", connerr, user, addr)
		return connerr
	}
	defer conn.Close()
	cli := pb.NewAuthServiceClient(conn)
	x, clierr := cli.ChangePass(context.Background(), &pb.ChangePassRequest{Uname: user, Passwd: passwd, NewPasswd: newpass})
	if clierr != nil {
		log.Infof("change password fail because %v. request %s from %s", clierr, user, addr)
		return clierr
	}
	err := x.GetErr()
	if err != "" {
		log.Infof("change password fail because %v. request %s from %s", err, user, addr)
		return errors.New(err)
	}
	return nil
}

func (ja *LocalAuth) Unload() error {
	ja.mu.RLock()
	defer ja.mu.RUnlock()
	b, err := json.Marshal(ja.keys)
	if err != nil {
		return err
	}
	return ioutil.WriteFile(ja.keyFile, b, 0600)
}

func (ja *LocalAuth) AddKey(uname, addr, key string) error {
	ja.mu.Lock()
	defer ja.mu.Unlock()
	if _, ok := ja.keys[uname]; !ok {
		ja.keys[uname] = make(map[string]string)
	}
	ja.keys[uname][choped(addr)] = key
	return nil
}

func choped(addr string) string {
	if strings.Contains(addr, ":") {
		s := strings.Split(addr, ":")
		return s[0]
	}
	return addr
}

func (ja *LocalAuth) AuthKey(uname, addr, key string) bool {
	ja.mu.RLock()
	defer ja.mu.RUnlock()
	if au, ok := ja.keys[uname]; ok {
		if k, ok := au[choped(addr)]; ok {
			return k == key
		}
	}
	return false
}

func salted(pass, salt string) string {
	md5sum := md5.Sum([]byte(pass + salt))
	return fmt.Sprintf("%x", md5sum)
}

func (ja *LocalAuth) AuthUserPass(user, passwd, addr string) bool {
	var rp string
	rp = "/" + addr + "/ssh?action=login"
	if ja.tkvalid == nil {
		ja.tkvalid = make(map[string]int64)
	}
	if ja.rpvalid == nil {
		ja.rpvalid = make(map[string][]string)
	}
	if ja.tkvalid[user] >= time.Now().UnixNano()/1e6 {
		ok := false
		for _, urp := range ja.rpvalid[user] {
			if urp == rp {
				ok = true
				break
			}
		}
		if ok {
			return true
		}
	} else {
		ja.rpvalid[user] = nil
	}

	ch := make(chan *naming.Update, 1)
	re := utils.NewResolver(ch)
	b := grpclb.NewBalancer(re, grpclb.NewRoundRobinSelector())
	ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
	conn, connerr := grpc.DialContext(ctx, *auth, grpc.WithInsecure(), grpc.WithBalancer(b))
	//	conn, connerr := grpc.Dial("10.19.156.37:50051", grpc.WithInsecure())

	if connerr != nil {
		log.Errorf("gprc connection error> %v", connerr)
		return false
	}
	defer conn.Close()
	cli := pb.NewAuthServiceClient(conn)
	x, clierr := cli.Auth(context.Background(), &pb.AuthRequest{Uname: user, Passwd: passwd})
	if clierr != nil {
		log.Errorf("auth [password] of <%s>, %v.\n\tbecause %v", user, false, clierr)
		return false
	}
	tk := x.GetToken()
	vu := x.GetValidUntil()
	autherr := x.GetErr()
	if autherr != "" {
		log.Errorf("auth [password] of <%s>, %v.\n\tbecause %v", user, false, autherr)
		return false
	}
	log.Infof("auth [password] of <%s>, %v.\n\tmessage ==> %v, ValidUntil: %v", user, true, tk, vu)
	atreq, err := cli.AuthToken(context.Background(), &pb.AuthTokenRequest{Token: tk, ResourcePath: rp})
	if err != nil {
		log.Errorf("auth [token] of <%s>, %v.\n\tbecause %v", user, false, err)
		return false
	}
	if !atreq.GetValid() {
		log.Errorf("auth [token] of <%s>, %v.\n\tbecause %s", user, false, atreq.GetErr())
		return false
	}
	log.Infof("valid time until %s\n", time.Unix(atreq.GetValidUntil()/1000, 0).Format("2006-01-02 03:04:05 PM"))
	ja.tkvalid[user] = atreq.GetValidUntil()
	ja.rpvalid[user] = append(ja.rpvalid[user], rp)
	return atreq.GetValid()
}

func (ja *LocalAuth) Reload() error {
	ja.mu.Lock()
	defer ja.mu.Unlock()
	b, err := ioutil.ReadFile(ja.keyFile)
	if err != nil {
		return fmt.Errorf("key file, %v", err)
	}
	if err = json.Unmarshal(b, &ja.keys); err != nil {
		return fmt.Errorf("key file, %v", err)
	}
	return nil
}
